/**
 * Package: vvg.lib.ol
 * 
 * OpenLayers related library functions and constants.
 * - create the custom map with vvg.lib.ol.createMap(...)
 * - change urls.wfs.ffsr and ol.presetDSDescriptor_AqWl.parametersWFS
 *   to use a different WFS url
 * - change ol.presetDSDescriptor_AqWl.boreDescriptor and the nested
 *   featureToBore(...) function to use a different data model
 * 
 * Relies on:
 * - vvg.lib.*
 * - OpenLayers
 * 
 * @author: King Lung Chiu
 */
(function(vvg){
   var app =  vvg.app;
   var lib = vvg.lib;
   var SPACER = '_>_';
   var VIZ_DATA_READY = lib.eventNames.VIZ_DATA_READY;
   var VIZ_DATA_FETCH_STARTED = lib.eventNames.VIZ_DATA_FETCH_STARTED;
   var VIZ_DATA_FETCH_COMPLETE = lib.eventNames.VIZ_DATA_FETCH_COMPLETE;
   var dataCategories = lib.bore.dataCategories;
   var vizUpdateTriggers = {
      NEW_TRANSECT: 'vvg_vizUpdateTrigger_NEW_TRANSECT'
      ,NEW_DISTANCE: 'vvg_vizUpdateTrigger_NEW_DISTANCE'
      ,NEW_DATERANGE: 'vvg_vizUpdateTrigger_NEW_DATERANGE'
   };

   var my = {
      getFeatureBoreId: function(feature) {return feature.attributes.bore_id;}
      ,getFeatureDate: function(feature) {return feature.attributes.date;}
      ,boreLabelField: 'label'
      ,boreDateField: 'groundwaterLevelDate'
      ,boreLinearDistanceField: 'linearDistance'
   };
   
   var urls = {
      wms : {
         ffsr:   'http://siss.versi.edu.au/geoserver/wms?service=WMS'
         ,ubs:    'http://maps.ubspatial.com.au/groundwater/wms?service=WMS'
      }
      ,wfs : {
         ffsr:   'http://siss.versi.edu.au/geoserver/wfs?service=WFS'
         ,ubs:    'http://maps.ubspatial.com.au:8000/geoserver/wfs?service=WFS'
         ,loc:    'http://localhost:8080/geoserver/wfs?service=WFS'
      }
   };
   var projections = {
      sphericalMercator: new OpenLayers.Projection('EPSG:900913')
      ,wgs84latlon: new OpenLayers.Projection('EPSG:4326')
   };
   // Map is in spherical mercator = Google.
   projections.map = projections.sphericalMercator;
   projections.mapSRS = projections.map.getCode();
   // Display is in WGS84 lat/lon.
   projections.display = projections.wgs84latlon;
   projections.displaySRS = projections.wgs84latlon.getCode();

   app.olMaps = [];
   app.olMapCount = 0;

   
   var ol = lib.ol = {      
      
      /**
       * List of default IDs for various map components
       */
      id: {
         MAP_DIV: 'map_element'  // default name
         ,LAYER_SWITCHER: function(MAP_DIV) {return (MAP_DIV + SPACER + 'map_layer_switcher');}
         ,NAV_CONTROL: function(MAP_DIV) {return (MAP_DIV + SPACER + 'map_nav_control');}
         ,VECTOR_LAYER: function(MAP_DIV) {return (MAP_DIV + SPACER + 'vector_layer');}
         ,VECTOR_LAYER_TOOLBAR: function(MAP_DIV) {return (MAP_DIV + SPACER + 'vector_layer_toolbar');}
      }
      
      ,projections: projections
      ,vizUpdateTriggers: vizUpdateTriggers
      
      /**
       * Generic event handler for OpenLayers UI events.
       * Useful when you simply want to check if an event is fired.
       * 
       * @param: event
       *    The OpenLayers event object that's automatically supplied for most
       *    events.
       */
      ,handle_allEvents: function(event) {
         lib.log(event.type);
      }
      
      ,getFeatureBoreId: my.getFeatureBoreId
      ,getFeatureDate: my.getFeatureDate
      ,featureDateComparator_Asc: lib.comparator_Asc(my.getFeatureDate)
      ,featureDateComparator_Desc: lib.comparator_Desc(my.getFeatureDate)

      ,uniqueFeatures_latest: function(allFeatures) {
         return lib.unique(allFeatures, ol.getFeatureBoreId, ol.featureDateComparator_Desc);
      }
      ,uniqueFeatures_oldest: function(allFeatures) {
         return lib.unique(allFeatures, ol.getFeatureBoreId, ol.featureDateComparator_Asc);
      }
      ,featuresInDateRange: function(allFeatures, startDate, endDate) {
         return lib.filter(allFeatures, function(feature) {
            var featureDate = feature.attributes.date;
            return (startDate <= featureDate && featureDate <= endDate);
         });
      }
      ,featuresWithinOffsetAndDateRange: function(
         allFeatures, offsetDistance, startDate, endDate) {
         return lib.filter(allFeatures, function(feature) {
            var featureDate = feature.attributes.date;
            return (startDate <= featureDate && featureDate <= endDate &&
                    Math.abs(feature.attributes.offset) <= offsetDistance);
         });
      }
      ,latestFeaturesWithinOffsetAndDateRange: function(
         allFeatures, offsetDistance, startDate, endDate) {
         return ol.uniqueFeatures_latest(
            ol.featuresWithinOffsetAndDateRange(allFeatures, offsetDistance, startDate, endDate)
         );
      }
      ,oldestFeaturesWithinOffsetAndDateRange: function(
         allFeatures, offsetDistance, startDate, endDate) {
         return ol.uniqueFeatures_oldest(
            ol.featuresWithinOffsetAndDateRange(allFeatures, offsetDistance, startDate, endDate)
         );
      }

      ,boreLabelField: my.boreLabelField
      ,boreDateField: my.boreDateField
      ,boreLinearDistanceField: my.boreLinearDistanceField
      ,boreDateComparator_Asc: lib.comparator_Asc(my.boreDateField)
      ,boreDateComparator_Desc: lib.comparator_Desc(my.boreDateField)
      ,boreLinearDistanceComparator_Asc: lib.comparator_Asc(my.boreLinearDistanceField)

      ,uniqueBores_latest: function(allBores) {
         return lib.unique(allBores, ol.boreLabelField, ol.boreDateComparator_Desc);
      }
      ,uniqueBores_oldest: function(allBores) {
         return lib.unique(allBores, ol.boreLabelField, ol.boreDateComparator_Asc);
      }
      ,boresWithinOffsetAndDateRange: function(
         allBores, offsetDistance, startDate, endDate) {
         return lib.filter(allBores, function(bore) {
            var boreDate = bore[ol.boreDateField];
            return (startDate <= boreDate && boreDate <= endDate &&
                    Math.abs(bore.offset) <= offsetDistance);
         });
      }
      ,latestBoresWithinOffsetAndDateRange: function(
         allBores, offsetDistance, startDate, endDate) {
         return ol.uniqueBores_latest(
            ol.boresWithinOffsetAndDateRange(allBores, offsetDistance, startDate, endDate)
         );
      }
      ,oldestBoresWithinOffsetAndDateRange: function(
         allBores, offsetDistance, startDate, endDate) {
         return ol.uniqueBores_oldest(
            ol.boresWithinOffsetAndDateRange(allBores, offsetDistance, startDate, endDate)
         );
      }
      
      /**
       * @public (currently unused)
       * Convenience method for combining multiple OpenLayers Filters with
       * an OpenLayers Logical AND Filter.
       * Useful for quickly reseting a layer's filter before forcing a refresh.
       * 
       * eg. to refresh a WFS layer with a dated DWithin filter:
       * 
       *    wfsLayer.filter = ol.AND([
       *       ol.BETWEEN('date', '2001-01-01', '2010-01-01')
       *       ,ol.DWITHIN(targetLineString, 0.05, 'degrees')]);
       *    wfsLayer.refresh({force: true});
       *    
       * @params: filters
       *    An array of filters to be combined by this Logical AND filter.
       */
      ,AND: function(filters) {
         return new OpenLayers.Filter.Logical({
            type: OpenLayers.Filter.Logical.AND
            ,filters: lib.isArray(filters)? filters : []
         });         
      }
      ,BETWEEN: function(property, start, end) {
         return new OpenLayers.Filter.Comparison({
            type: OpenLayers.Filter.Comparison.BETWEEN
            ,property: property
            ,lowerBoundary: start
            ,upperBoundary: end
         });
      }
      ,INTERSECTS: function(targetGeometry) {
         return new OpenLayers.Filter.Spatial({
            type: OpenLayers.Filter.Spatial.INTERSECTS
            ,value: targetGeometry
            ,projection: projections.mapSRS
         });
      }
      ,DWITHIN: function(targetGeometry, buffer, bufferUnits) {
         return new OpenLayers.Filter.Spatial({
            type: OpenLayers.Filter.Spatial.DWITHIN
            ,value: targetGeometry
            ,projection: projections.mapSRS
            ,distance: buffer
            ,distanceUnits: bufferUnits
         });
      }

      /**
       * @public (currently unused)
       * Creates an OpenLayers filter that selects features based on DWITHIN and
       * start/end dates. The target geometry for DWITHIN is assumed to be
       * a single line segment.
       * 
       * Used by the filtered WFS overlay to show features that fall near
       * a user-sketched line segment within a fixed date range.
       * 
       * @param: aX
       *    X value of the target feature's first vertex.
       *    Needs to be in the same unit of measure as the map's projection.
       *    
       * @param: aY
       *    Y value of the target feature's first vertex.
       *    Needs to be in the same unit of measure as the map's projection.
       *    
       * @param: bX
       *    X value of the target feature's second vertex.
       *    Needs to be in the same unit of measure as the map's projection.
       *    
       * @param: bY
       *    Y value of the target feature's second vertex.
       *    Needs to be in the same unit of measure as the map's projection.
       *    
       * @param: buffer
       *    Buffer size to use for DWITHIN. The unit of measure is in degrees
       *    (long/lat), not in KMs.
       *    
       * @param: startDate
       *    A W3C date string, eg. '2010-01-01'.
       *    Features with 'date' before startDate are discarded.
       *    
       * @param: endDate
       *    A W3C date string, eg. '2011-01-01'.
       *    Features with 'date' on or after endDate are discarded.
       */
      ,createDatedDWithin: function(aX, aY, bX, bY, buffer, startDate, endDate) {
         return new OpenLayers.Filter.Logical({
            type: OpenLayers.Filter.Logical.AND
            ,filters: [
               new OpenLayers.Filter.Spatial({
                  type: OpenLayers.Filter.Spatial.DWITHIN
                  ,value: new OpenLayers.Geometry.LineString([
                     new OpenLayers.Geometry.Point(aX, aY)
                     ,new OpenLayers.Geometry.Point(bX, bY)
                  ])
                  ,distance: buffer
               })
               ,new OpenLayers.Filter.Comparison({
                  type: OpenLayers.Filter.Comparison.BETWEEN
                  ,property: "date"
                  ,lowerBoundary: startDate
                  ,upperBoundary: endDate
               })
            ]
         });
      }

      /**
       * @public
       * Creates a custom OpenLayers map.
       * 
       * @param: MAP_DIV
       * 
       * @param: listeners
       */
      ,createMap: function(MAP_DIV, listeners) {
         app.olMapCount += 1;         
         MAP_DIV = MAP_DIV || (ol.id.MAP_DIV +'_'+ app.olMapCount);

         var layers = ol.createDefaultLayers();
         var map = new OpenLayers.Map(MAP_DIV, {
            projection: projections.map
            ,displayProjection: projections.display
//            projection: new OpenLayers.Projection('EPSG:900913')
//            ,displayProjection: new OpenLayers.Projection('EPSG:4326')
            ,layers: layers.array
            ,controls: [
               new OpenLayers.Control.LayerSwitcher({id: ol.id.LAYER_SWITCHER(MAP_DIV)})
//               ,new OpenLayers.Control.MousePosition()
               ,new OpenLayers.Control.PanZoomBar()
               ,new OpenLayers.Control.ScaleLine({geodesic: true})
               ,ol.createDrawingToolbar(MAP_DIV, layers.overlays.drawing)
            ]
         });
         ol.attachVVGMethods(map);
         ol.registerExternalEventListeners(map, listeners);
         
         layers.overlays.drawing.events.register(
            'sketchcomplete', layers.overlays.drawing, ol.handle_sketchcomplete);
//         layers.overlays.filtered.events.register(
//            'loadend', map, map.vvg_updateViz_stage2);

         map.vvg_appData = {
            // user-adjustable values
            // MUST convert to degrees first if directly used with WFS DWITHIN
            vizBuffer: {
               value: 5
               ,units: 'km'
            }
            // maximum allowed buffer size to use in caching / prefetching
            // MUST convert to degrees first if directly used with WFS DWITHIN
            ,vizBufferMax: {
               value: 5
               ,units: 'km'
            }
            ,vizDates: {
               start: '2000-01-01'
               ,end: '2013-01-01'
            }
            ,vizData: {
               targetTransect: null
               ,allFeatures: []        // all features currently in cache
               ,allBores: []           // all bores currently in cache
               ,selectedFeatures: []   // features selected for display
               ,selectedBores: []      // bores selected for charting
            }
            ,datastore: lib.dataStore(ol.presetDSDescriptor_AqWl)
            ,boreTransect: null
            ,baselayers: layers.baselayers
            ,overlays: layers.overlays
            ,selectFeatureControl: new OpenLayers.Control.SelectFeature(layers.overlays.filtered)
            // Note: Google.v3 uses EPSG:900913 as projection, so make sure
            // coordinates are transformed to this before panning / setting center.
            ,home: { // Victoria
               lonlat: new OpenLayers.LonLat(16087764.528886, -4471402.2969667).transform(
                  projections.sphericalMercator, map.getProjectionObject()
               )
               ,zoom: 8
            }
//            ,home: { // Italy
//               lonlat: new OpenLayers.LonLat(10.2, 48.9).transform(
//                  projections.wgs84latlon, map.getProjectionObject()
//               )
//               ,zoom: 5
//            }
         };

         map.vvg_panToHome();

         app.olMaps.push(map);
         return map;
      }
      
      ,createDefaultLayers: function() {
         var baseLayers = {
            gStreet: new OpenLayers.Layer.Google(
               "Google Streets", // the default
               {numZoomLevels: 20})
            ,gPhysical: new OpenLayers.Layer.Google(
               "Google Physical",
               {type: google.maps.MapTypeId.TERRAIN})
            ,gHybrid: new OpenLayers.Layer.Google(
               "Google Hybrid",
               {type: google.maps.MapTypeId.HYBRID, numZoomLevels: 20})
            ,gSatellite: new OpenLayers.Layer.Google(
               "Google Satellite",
               {type: google.maps.MapTypeId.SATELLITE, numZoomLevels: 22})               
         };        
         var overlays = {
            drawing: new OpenLayers.Layer.Vector('Transect', {
               eventListeners: {
                  visibilitychanged: ol.handle_visibilitychanged_vectorLayer}
               ,styleMap: new OpenLayers.StyleMap({
                  "default": new OpenLayers.Style({
                     strokeColor: "#ff0000"  // red
                     ,strokeWidth: 1
                     ,strokeOpacity: 1
                  })
               })
            })
            ,boreholeClustered: ol.createBoreholeLayerClustered()
            ,borehole: ol.createBoreholeLayer()
            ,filtered: new OpenLayers.Layer.Vector('Selected Bores', {
//               strategies: [new OpenLayers.Strategy.BBOX()]
//               ,protocol: new OpenLayers.Protocol.WFS({
//                  version: '1.1.0'
//                  ,url: urls.wfs.ffsr
//                  ,featureType: 'mv_vvg_bores_aq_wl'
//                  ,srsName: projections.mapSRS
//                  ,formatOptions: {
//                     srsName: projections.mapSRS
//                     ,extractAttributes: true
//                  }
//               })
//               // select nothing at the start
//               ,filter: ol.createDatedDWithin(0,0,0,0,0,'0','0')
               styleMap: new OpenLayers.StyleMap({
                  "default": new OpenLayers.Style({
                     pointRadius: 3
                     ,fillColor: "#0000ff"   // blue
                     ,fillOpacity: 0.8
                     ,strokeColor: "#0000ff"
                     ,strokeWidth: 1
                     ,strokeOpacity: 1
                  })
                  ,"select": new OpenLayers.Style({
                     pointRadius: 6
//                     ,fillColor: "#32a8a9" // medium blue
                     ,fillColor: "#8aeeef"   // light blue
                     ,fillOpacity: 1
//                     ,strokeColor: "#8aeeef"
                     ,strokeColor: "#32a8a9"
                     ,strokeWidth: 2
                     ,strokeOpacity: 1
                  })
               })
            })
         };
         var layers = [
            baseLayers.gHybrid
            ,baseLayers.gPhysical
            ,baseLayers.gStreet
            ,baseLayers.gSatellite
            ,overlays.boreholeClustered
            ,overlays.borehole
            ,overlays.drawing
            ,overlays.filtered
         ];
         for (var i=0; i<layers.length; ++i) {
               layers[i].animationEnabled = true;
               layers[i].sphericalMercator = true;
               layers[i].transitionEffect = "resize";
         }
         
         return {
            array: layers
            ,baselayers: baseLayers
            ,overlays: overlays
         };
      }
      
      ,createBoreholeLayerClustered: function() {
         var clusterDistance = 41;
         var style = new OpenLayers.Style({
            pointRadius: "${radius}"
            ,fillColor: "#ffcc66"
            ,fillOpacity: 0.8
            ,strokeColor: "#cc6633"
            ,strokeWidth: "${width}"
            ,strokeOpacity: 0.9
            ,label: '${label}'
         }, {
            context: {
               label: function(feature) {
                  return feature.cluster? feature.attributes.count : 1;
               }
               ,width: function(feature) {
                  return (feature.cluster) ? 2 : 1;
               }
               ,radius: function(feature) {
                  var map = feature.layer.map;
                  var pix = 3;
                  var count = 0;
                  var i = 0;
                  var point = new OpenLayers.Geometry.Point(0,0);
                  var minX = 0, maxX = 0;
                  var minY = 0, maxY = 0;
                  var radX = 0, radY = 0;
                  var minPixel, maxPixel;

                  // calculate accurate feature radius from the extreme x & y
                  // values of the clustered points
                  if(feature.cluster) {
                     count = feature.cluster.length;
                     point = feature.cluster[0].geometry;
                     minX = maxX = point.x;
                     minY = maxY = point.y;                     
                     for(i = 1; i < count; i++) {
                        point = feature.cluster[i].geometry;
                        if(point.x < minX)
                           minX = point.x;
                        else if(point.x > maxX)
                           maxX = point.x;
                        if(point.y < minY)
                           minY = point.y;
                        else if(point.y > maxY)
                           maxY = point.y;
                     }
                     minPixel = map.getViewPortPxFromLonLat(
                        new OpenLayers.LonLat(minX, minY));
                     maxPixel = map.getViewPortPxFromLonLat(
                        new OpenLayers.LonLat(maxX, maxY));
                     radX = Math.ceil(Math.abs((maxPixel.x - minPixel.x) / 2));
                     radY = Math.ceil(Math.abs((maxPixel.y - minPixel.y) / 2));
                     pix += Math.max(radX, radY);
//                     lib.log('radX: '+ radX +', radY: '+ radY +', pix: '+ pix);
                  }

                  return pix;
               }
            }
         });
         
         var borehole = new OpenLayers.Layer.Vector('All Bores (Clusterd)', {
            strategies: [
               new OpenLayers.Strategy.BBOX()
               ,new OpenLayers.Strategy.Cluster({
                  distance: clusterDistance
                  ,threshold: 3})
            ]
            ,protocol: new OpenLayers.Protocol.WFS({
               version: '1.1.0'
               ,url: urls.wfs.ffsr
               ,featureType: 'mv_vvg_bores_aq_wl1'
//               ,featureType: 'mv_vvg_bores_aq_wl'
//               ,featureType: 'Borehole'
               ,srsName: projections.mapSRS
               ,formatOptions: {
                  srsName: projections.mapSRS
                  ,extractAttributes: true
               }
            })
            ,styleMap: new OpenLayers.StyleMap({
               "default": style
               ,"select": {
                  fillColor: "#8aeeef"
                  ,strokeColor: "#32a8a9"
               }
//            })
//            ,filter: new OpenLayers.Filter.Comparison({
//               type: OpenLayers.Filter.Comparison.BETWEEN
//               ,property: "date"
//               ,lowerBoundary: '2010-01-01'
//               ,upperBoundary: '2013-01-01'
//               ,lowerBoundary: '1990-01-01'
//               ,upperBoundary: '1995-01-01'
//               ,lowerBoundary: '1990-01-01T00:00:00+10:00'
//               ,upperBoundary: '1991-01-01T00:00:00+10:00'
//               ,property: "dateOfDrilling"
//               ,lowerBoundary: '1990'
//               ,upperBoundary: '1991'
            })
         });

         return borehole;
      }
      
      ,createBoreholeLayer: function() {
         var borehole = new OpenLayers.Layer.Vector('All Bores', {
            strategies: [new OpenLayers.Strategy.BBOX()]
            ,visibility: false
            ,protocol: new OpenLayers.Protocol.WFS({
               version: '1.1.0'
               ,url: urls.wfs.ffsr
               ,featureType: 'mv_vvg_bores_aq_wl1'
               ,srsName: projections.mapSRS
               ,formatOptions: {
                  srsName: projections.mapSRS
                  ,extractAttributes: true
               }
            })
            ,styleMap: new OpenLayers.StyleMap({
               "default": new OpenLayers.Style({
                  pointRadius: 1
                  ,fillColor: "#ff0000"
                  ,fillOpacity: 0.8
                  ,strokeColor: "#ff0000"
                  ,strokeWidth: 1
                  ,strokeOpacity: 0.9
               })
            })
         });

         return borehole;
      }

      ,createDrawingToolbar: function(MAP_DIV, drawingLayer) {
         var navControl = new OpenLayers.Control.Navigation();
         var lineControl = new OpenLayers.Control.DrawFeature(
            drawingLayer
            ,OpenLayers.Handler.Path
            ,{
               displayClass: 'olControlDrawFeaturePath'
               ,handlerOptions: {
                  maxVertices: 2
               }
               /**
                * Registering featureadded handler here for the line drawing
                * control doesn't work as expected: on invocation, handler's
                * event object only has the 'geometry' object, not the higher
                * level 'feature' object required to allow adding/removing
                * from the vector layer.
                * 
                * Register handler via control.events.register() instead.
                */  
               //,featureAdded: ol.handle_featureadded
            }
         );
         
         var toolbar = new OpenLayers.Control.Panel({
            displayClass: 'olControlEditingToolbar'
            ,defaultControl: navControl
            ,id: ol.id.VECTOR_LAYER_TOOLBAR(MAP_DIV)
         });
         toolbar.addControls([navControl,lineControl]);
         
         return toolbar;
      }

      /**
       * Event handler to toggle the custom map's toolbar when the user switches
       * the drawing layer on or off.
       * 
       * When the layer is turned off:
       *    - the toolbar is turned off,
       *    - the toolbar's default control is activated
       *      (and all other toolbar controls are turned off)
       * When the layer is turned on:
       *    - the toolbar is turned on
       *    
       * @param: event
       *    The visibilitychanged event from the vector layer.
       */
      ,handle_visibilitychanged_vectorLayer: function(event) {
         var layer = event.object;
         var map = layer.map;
         var toolbar = map.getControl(ol.id.VECTOR_LAYER_TOOLBAR(map.div.id));
         if(layer === map.vvg_appData.overlays.drawing) {
            if(layer.visibility === true) {
               toolbar.activate();
            } else {
               toolbar.deactivate();
               toolbar.defaultControl.activate();
            }            
         }
      }
      
      /**
       * Event handler to trigger a visualisation update on the custom map,
       * when the user draws a line on the drawing layer.
       * 
       * Replaces the deprecated ol.handle_featureadded() handler
       * 
       * @param: event
       *    The sketchcomplete event from the vector layer.
       *    Relies on :
       *       - existence of event.feature
       *       - this being the vector layer
       */
      ,handle_sketchcomplete: function(event) {
         var newFeature = event.feature;
         var drawingLayer = this;
         var map = drawingLayer.map;
         var appData = map.vvg_appData;

         // clear existing features
         drawingLayer.destroyFeatures();

         // record new feature for remaining steps of the viz refresh pipeline,
         // then trigger the rest of the refresh pipeline
         appData.vizData.targetTransect = lib.transect(newFeature);
         map.vvg_updateViz(vizUpdateTriggers.NEW_TRANSECT);

         // DEBUG: log new feature's geometry
         if(app.debug === true) {
            lib.log('\n[lib.ol.handle_featureadded] new feature: ');
            var i = 0;
            var vertices = newFeature.geometry.getVertices();
            for(i = 0; i < vertices.length; i++) {
               lib.log(' vertex ' + i );
               lib.log('  x: ' + vertices[i].x);
               lib.log('  y: ' + vertices[i].y);
            }            
         }
      }

      /**
       * Adds VVG-specific methods to the given map object. See the individual
       * methods for details.
       * 
       * @param: map
       *    The OpenLayers Map object to add extension methods to.
       */
      ,attachVVGMethods: function(map) {
         
         map.vvg_panToHome = function() {
            var home = this.vvg_appData.home;
            this.setCenter(home.lonlat, home.zoom);
         };

         /**
          * Updates the buffer value for use in the WFS DWITHIN query.
          * Optionally triggers the remaining part of the map's visualisation
          * refresh pipeline.
          * 
          * @param: newValue
          *    The new value for the DWITHIN buffer.
          *
          * @param: notifyExternalListeners
          *    Optional parameter.
          *    If true, triggers the rest of the visualisation refresh pipeline
          *    by notifying external listeners of VIZ_DATA_READY.
          *    If undefined or set to any other value, the external listeners
          *    are not notified.
          */
         map.vvg_updateVizBuffer = function(newValue, notifyExternalListeners) {
            this.vvg_appData.vizBuffer.value = newValue;
            this.vvg_updateViz(vizUpdateTriggers.NEW_DISTANCE, notifyExternalListeners);
//            if(continueVizRefresh === true) {
//               this.vvg_updateViz(vizUpdateTriggers.NEW_DISTANCE);               
//            }
         };

         /**
          * Updates the date values for filtering the WFS layers.
          * Optionally triggers the remaining part of the map's visualisation
          * refresh pipeline.
          * 
          * @param: newStart
          *    The new value for the start of the date range.
          *
          * @param: newEnd
          *    The new value for the end of the date range.
          *
          * @param: notifyExternalListeners
          *    Optional parameter.
          *    If true, triggers the rest of the visualisation refresh pipeline
          *    by notifying external listeners of VIZ_DATA_READY.
          *    If undefined or set to any other value, the external listeners
          *    are not notified.
          */
         map.vvg_updateVizDates = function(newStart, newEnd, notifyExternalListeners) {
            var vizDates = this.vvg_appData.vizDates;
            vizDates.start = newStart;
            vizDates.end = newEnd;
            this.vvg_updateViz(vizUpdateTriggers.NEW_DATERANGE, notifyExternalListeners);
//            if(continueVizRefresh === true) {
//               this.vvg_updateViz(vizUpdateTriggers.NEW_DATERANGE);               
//            }
         };

         map.vvg_updateViz = function(trigger, notifyExternalListeners) {
            var map = this;
            switch(trigger){
               case vizUpdateTriggers.NEW_TRANSECT:
                  map.vvg_reloadDatastore();
                  break;
               case vizUpdateTriggers.NEW_DISTANCE:
                  map.vvg_redrawSelectedBores(notifyExternalListeners);
                  break;
               case vizUpdateTriggers.NEW_DATERANGE:
                  map.vvg_redrawSelectedBores(notifyExternalListeners);
                  break;
               default:
                  throw {
                     name: 'IllegalArgumentException'
                     ,message: 'vvg.lib.ol/map.vvg_updateViz(): illegal trigger: ' + trigger.toString()
                  };
                  break;
            }
         };

         map.vvg_reloadDatastore = function() {
            var map = this;
            var appData = map.vvg_appData;
            var vizData = appData.vizData;
            var datastore = appData.datastore;
            var targetTransect = vizData.targetTransect;
            var existingFeatures = vizData.allFeatures;
            var i = 0, len = existingFeatures.length;
            
            // release memory from all existing features
            for(i = 0; i < len; i++) {
               existingFeatures[i].destroy();
            }
            
            // notify listeners remote fetching is about to start
            map.events.triggerEvent(VIZ_DATA_FETCH_STARTED, {
               debugMsg: 'triggered from lib.ol/map.vvg_reloadDatastore()'
            });

            // then reload list of matching bores
            datastore.reload(
               targetTransect, appData.vizBufferMax, function() {
                  vizData.allFeatures = datastore
                     .allFeatures()
                     .sort(ol.featureDateComparator_Asc);
                  appData.boreTransect = datastore.boreTransect();
                  vizData.allBores = appData.boreTransect
                     .getBores()
                     .sort(ol.boreDateComparator_Asc);
                  
                  // and continue the rest of the visualisation refresh pipeline
                  map.vvg_redrawSelectedBores(true);
               }			
            );            
         };
         
         map.vvg_redrawSelectedBores = function(notifyExternalListeners) {
            // do nothing when transect hasn't been drawn yet
            if(!this.vvg_appData.vizData.targetTransect){
               if(app.debug === true) {
                  lib.log(
                     'vvg.lib.ol/map.vvg_redrawSelectedBores():\n' +
                     ' no transect drawn yet, skipping redraw'
                  );
               }
               return;
            }

            var map = this;
            var appData = map.vvg_appData;
            var vizDates = appData.vizDates;
            var vizBuffer = appData.vizBuffer;
            var vizData = appData.vizData;
            var targetTransect = vizData.targetTransect;
            var filteredWFSLayer = appData.overlays.filtered;
            var normalisedOffset = targetTransect.normaliseDistance(vizBuffer).value;

            // select qualifying features and bores
            vizData.selectedFeatures = ol.latestFeaturesWithinOffsetAndDateRange(
               vizData.allFeatures
               ,normalisedOffset
               ,vizDates.start
               ,vizDates.end
            );
            var selectedBores = ol.latestBoresWithinOffsetAndDateRange(
               vizData.allBores
               ,normalisedOffset
               ,vizDates.start
               ,vizDates.end
            );
            // clean up: keep only 1 when multiple bores have the same
            // linearDistance - can't plot more than 1 for each x value
            vizData.selectedBores = lib.unique(
               selectedBores
               ,ol.boreLinearDistanceField   // keep only 1 when 1+ bores have
               ,ol.boreDateComparator_Desc   // same linearDistance
            ).sort(ol.boreLinearDistanceComparator_Asc);

            // redraw selected features
            filteredWFSLayer.removeAllFeatures();
            filteredWFSLayer.addFeatures(vizData.selectedFeatures);

            if(notifyExternalListeners === true) {
               // notify listeners remote fetching is complete
               map.events.triggerEvent(VIZ_DATA_FETCH_COMPLETE, {
                  debugMsg: 'triggered from lib.ol/map.vvg_redrawSelectedBores()'
               });

               // notify listeners new vizData is available
               map.events.triggerEvent(VIZ_DATA_READY, {
                  targetTransect: targetTransect
                  ,selectedBores: vizData.selectedBores
                  ,debugMsg: 'triggered from lib.ol/map.vvg_redrawSelectedBores()'
               });               
            }
         };

         /**
          * Redraws the OpenLayers map. Call this when the component
          * is resized, so the map can display properly again.
          * 
          * @param: targetDiv
          *    The container div that this map should render itself to.
          *    The map's existing div is used if targetDiv is not supplied.
          */
         map.vvg_redrawMap = function(targetDiv) {
            var map = this;
            targetDiv = targetDiv || map.div.id;

            // probably redundant
            map.render(targetDiv);

            // required
            for (var i = 0; i < map.layers.length; i++) {
               if(map.layers[i].getVisibility() === true)
               map.layers[i].redraw();            
            }
         };

         /**
          * Highlights a given bore on the map.
          * 
          * @param {string} boreId
          *    ID of the bore to highlight.
          */
         map.vvg_selectBore = function(boreId) {
            var map = this;
            var appData = map.vvg_appData;
            var selectFeatureControl = appData.selectFeatureControl;
            var boreFeature = lib.find(appData.vizData.selectedFeatures, function(f){
               return my.getFeatureBoreId(f) === boreId;
            });
            
            if(boreFeature){
               selectFeatureControl.select(boreFeature);
            } else {
               lib.log(
                  '[error] vvg.lib.ol/map.vvg_selectBore():\n'+
                  ' no feature found with bore ID "'+ boreId +'"'
               );
            }
         };

         /**
          * Un-highlights a given bore on the map.
          * 
          * @param {string} boreId
          *    ID of the bore to un-highlight.
          */
         map.vvg_unselectBore = function(boreId) {
            var map = this;
            var appData = map.vvg_appData;
            var selectFeatureControl = appData.selectFeatureControl;
            var boreFeature = lib.find(appData.overlays.filtered.selectedFeatures, function(f){
               return my.getFeatureBoreId(f) === boreId;
            });
            
            if(boreFeature){
               selectFeatureControl.unselect(boreFeature);
            } else {
               lib.log(
                  '[error] vvg.lib.ol/map.vvg_unselectBore():\n'+
                  ' no feature found with bore ID "'+ boreId +'"'
               );
            }
         };
      }

      ,registerExternalEventListeners: function(map, listeners){
         var eventName = null;
         var handler = null;
         var mapEvents = map.events;

         for(eventName in listeners){
            if(listeners.hasOwnProperty(eventName)){
               handler = listeners[eventName];
               if(typeof handler === 'function'){
                  if(eventName === VIZ_DATA_READY ||
                     eventName === VIZ_DATA_FETCH_STARTED ||
                     eventName === VIZ_DATA_FETCH_COMPLETE){
                     mapEvents.addEventType(eventName);                     
                  }
                  mapEvents.register(eventName, map, handler);                  
               }
//               if(typeof handler === 'function' && (
//                  eventName === VIZ_DATA_READY ||
//                  eventName === VIZ_DATA_FETCH_STARTED ||
//                  eventName === VIZ_DATA_FETCH_COMPLETE
//               )) {
//                  mapEvents.addEventType(eventName);
//                  mapEvents.register(eventName, map, handler);                  
//               }
            }
         }
      }
      
      ,presetDSDescriptor_AqWl: {
         parametersWFS: {
            // This WFS featureType (i.e. table) returns the following attributes:
            // bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
            // aquifername, from_depth, to_depth, date, depth, reduced_level, natsurf_level, 
            name: 'DPI-Vic bores WFS',
            serverParams: {
               // GetCapabilities is not reliable, especially if a SISS implementation is used.
               bestDistanceFilter: OpenLayers.Filter.Spatial.INTERSECTS,
               //bestDistanceFilter: OpenLayers.Filter.Spatial.DWITHIN,  
               //bestDistanceFilter: OpenLayers.Filter.Spatial.BBOX,
               units: 'degrees'  // Could, in theory, be obtained from the WFS with a GetCapabilities. Hardcoded here, for performance.
            },
            params: {
               url: urls.wfs.ffsr,
               featureType: 'mv_vvg_bores_aq_wl',
               geometryName: 'geom_loc',
               extractAttributes : true,
               srsName: 'EPSG:900913',  // SRS of the map.
               version: '1.1.0'
            }
         },
         boreDescriptor: {
            bore_id: {
               dataCategory: dataCategories.DESCRIPTOR,
               attemptInterpolate: false
            },
            label: {
               dataCategory: dataCategories.DESCRIPTOR,
               attemptInterpolate: false,
               defaultValue: 'Bore ?'
            },
            linearDistance: {
               dataCategory: dataCategories.DESCRIPTOR
            },
            offset: {
               dataCategory: dataCategories.DESCRIPTOR
            },
            elevation: {
               dataCategory: dataCategories.DESCRIPTOR,
               zeroAllowed: false,
               defaultValue: 100.0
            },
            boreDepth: {
               dataCategory: dataCategories.DESCRIPTOR
            },
            groundwaterLevel: {
               dataCategory: dataCategories.LEVEL,
               interfaceIndex: 0,
               style: 'blue' // Or anything else that allows to style the level.
            },
            groundwaterLevelDate: {
               dataCategory: dataCategories.DESCRIPTOR,
//               dataCategory: dataCategories.DATE,
               relatesTo: 'groundwaterLevel',
               attemptInterpolate: false,
               zeroAllowed: false,
               defaultValue: '1970-01-01T00:00:00+10:00',
               dateFormat: undefined
               // If 'boreDescriptor[key].dateFormat' is provided, parsing is done with
               // date.js by Matt Kruse http://www.mattkruse.com/javascript/date/
               // Mismatches between input and format are not tolerated.
               // If no 'dateFormat' is given JavaScript's 'Date.parse()' is used.
               // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/parse
            },
            aquiferTopLevel: {
               dataCategory: dataCategories.LEVEL
            },
            aquiferBottomLevel: {
               dataCategory: dataCategories.LEVEL
            }
         },
         featureToBore: function (feature) {
            // Fields of table mv_vvg_bores_aq_wl:
            // bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
            // aquifername, from_depth, to_depth, date, depth, reduced_level, natsurf_level.
            var bore_id = feature.attributes.bore_id;
            var rlns = +feature.attributes.rlns;
            var bdepth = +feature.attributes.bdepth;
            var case_height = +feature.attributes.case_height;
            var from_depth = +feature.attributes.from_depth;
            var to_depth = +feature.attributes.to_depth;
            var date = feature.attributes.date;
            var depth = +feature.attributes.depth;
            // Automatically added.
            var linearDistance = feature.attributes.linearDistance;
            var offset = feature.attributes.offset;

            var aqTop, aqBot;
            if (from_depth) {
               aqTop = rlns-from_depth;
            } else {
               aqTop = rlns;
            }
            if (to_depth) {
               aqBot = rlns-to_depth;
            } else {
               aqBot = rlns-bdepth;  // Assume depth of bore if no aquifer bottom defined.
            }

            var bore = {
               bore_id: bore_id,
               label: 'Bore '+bore_id,
               linearDistance: linearDistance,
               offset: offset,
               elevation: rlns,
               boreDepth: bdepth,
               groundwaterLevel: rlns-(depth-case_height),
               groundwaterLevelDate: date.slice(0,10), // only want 'YYYY-MM-DD'
               aquiferTopLevel: aqTop,
               aquiferBottomLevel: aqBot
            };
            return bore;
         }
      }
   };
   
}(window.vvg = window.vvg || {}));